<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    View
 * @subpackage Helper
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * View helper for handling Ext JS operations
 *
 * @category   LOSC Framework
 * @package    View
 * @subpackage Helper
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
class Losc_View_Helper_Ext extends Zend_View_Helper_Abstract
{
    const ADAPTER_EXT       = 'ext';
    CONST ADAPTER_JQUERY    = 'jquery';
    CONST ADAPTER_PROTOTYPE = 'prototype';
    CONST ADAPTER_YUI       = 'yui';
    
    /**
     * Whether or not ExtJS is enabled
     * 
     * @var bool
     */
    protected $_enabled = false;
    
    /**
     * Path to ExtJS
     *
     * @var string
     */
    protected $_path = '/assets/ext-2.2/';
    
    /**
     * Adpater to use
     *
     * @var string
     */
    protected $_adapter = self::ADAPTER_EXT;
    
    /**
     * Whether ext-all.js or ext-core.js should be loaded
     *
     * @var bool
     */
    protected $_loadAll = true;
    
    /**
     * Whether or not debug version of ext-all/ext-core should be loaded
     *
     * @var bool
     */
    protected $_loadDebug = false;
    
    /**
     * Locking mechanism for capture methods to prevent nested captures
     * 
     * @var bool
     */
    protected $_captureLock = false;

    /**
     * Arbitrary javascript to include in ExtJS script
     * 
     * @var array
     */
    protected $_javascriptStatements = array();

    /**
     * Actions to perform on Ext.onReady
     * 
     * @var array
     */
    protected $_onReadyActions = array();

    /**
     * Register the ExtJS ext-all.css stylesheet?
     * 
     * @var bool
     */
    protected $_registerExtStylesheet = true;

    /**
     * Local stylesheets
     * 
     * @var array
     */
    protected $_stylesheets = array();
    
    /**
     * Returns view helper instance
     *
     * @return Losc_View_Helper_Ext
     */
    public function ext()
    {
        return $this;
    }
    
    /**#@+
     * Method group: Basic helper setup.
     */

    /**
     * Enable ExtJS
     * 
     * @return Losc_View_Helper_Ext
     */
    public function enable()
    {
        $this->_enabled = true;
        return $this;
    }

    /**
     * Disable ExtJS
     * 
     * @return Losc_View_Helper_Ext
     */
    public function disable()
    {
        $this->_enabled = false;
        return $this;
    }

    /**
     * Is ExtJS enabled?
     * 
     * @return bool
     */
    public function isEnabled()
    {
        return $this->_enabled;
    }
    
    /**
     * Sets path to use for retrieving ExtJS assets
     *
     * @param  string $path  path to ExtJS
     * @return Losc_View_Helper_Ext
     */
    public function setPath($path)
    {
        $this->_path = rtrim((string) $path, '/') . '/';
        return $this;
    }
    
    /**
     * Returns path to use for retrieving ExtJS assets
     *
     * @return string
     */
    public function getPath()
    {
        return $this->_path;
    }
    
    /**
     * Sets adapter to use when loading ExtJS
     * 
     * The following class constants may be used for convenience:
     * <ul>
     * <li>{@link ADAPTER_EXT}</li>
     * <li>{@link ADAPTER_JQUERY}</li>
     * <li>{@link ADAPTER_PROTOTYPE}</li>
     * <li>{@link ADAPTER_YUI}</li>
     * </ul>
     *
     * @param string $adapter  adapter to use when loading ExtJS
     * @return Losc_View_Helper_Ext
     */
    public function setAdapter($adapter)
    {
        $adapter = strtolower($adapter);
        
        switch ($adpater) {
            case self::ADAPTER_EXT:
            case self::ADAPTER_JQUERY:
            case self::ADAPTER_PROTOTYPE:
            case self::ADAPTER_YUI:
                break;
            default:
                $msg = array('Unknown adapter (%s)', $adapter);
                throw new Losc_View_Exception($msg);
                break;
        }
        
        $this->_adapter = $adapter;
        
        return $this;
    }
    
    /**
     * Returns adapter to use when loading ExtJS
     *
     * @return string
     */
    public function getAdapter()
    {
        return $this->_adapter;
    }

    /**
     * Load ext-all.js or ext-debug.js?
     *
     * With no arguments, returns the status of the flag; with arguments, sets 
     * the flag and returns the object.
     * 
     * @param  null|bool $flag
     * @return Losc_View_Helper_Ext|bool
     */
    public function loadAll($flag = null)
    {
        if (null === $flag) {
             return $this->_loadAll;
        }

        $this->_loadAll = (bool) $flag;
        return $this;
    }

    /**
     * Load debug versions of ext-all.js/ext-debug.js?
     *
     * With no arguments, returns the status of the flag; with arguments, sets 
     * the flag and returns the object.
     * 
     * @param  null|bool $flag
     * @return Losc_View_Helper_Ext|bool
     */
    public function loadDebug($flag = null)
    {
        if (null === $flag) {
             return $this->_loadDebug;
        }

        $this->_loadDebug = (bool) $flag;
        return $this;
    }
    
    /**#@+
     * Method group: Ext.onReady handling.
     */

    /**
     * Add a script to execute onReady
     *
     * Ext.onReady accepts:
     * - function name
     * - lambda
     * 
     * @param  string $callback Lambda
     * @return Losc_View_Helper_Ext
     */
    public function addOnReady($callback)
    {
        if (!in_array($callback, $this->_onReadyActions, true)) {
            $this->_onReadyActions[] = $callback;
        }
        return $this;
    }

    /**
     * Retrieve all registered onReady actions
     * 
     * @return array
     */
    public function getOnReadyActions()
    {
        return $this->_onReadyActions;
    }

    /**
     * Start capturing routines to run onReady
     * 
     * @return bool
     * @throws Losc_View_Exception  if trying to nest captures
     */
    public function onReadyCaptureStart()
    {
        if ($this->_captureLock) {
            throw new Losc_View_Exception('Cannot nest onReady captures');
        }

        $this->_captureLock = true;
        return ob_start();
    }

    /**
     * Stop capturing routines to run onReady
     * 
     * @return bool
     */
    public function onReadyCaptureEnd()
    {
        $data               = ob_get_clean();
        $this->_captureLock = false;

        $this->addOnReady(trim($data));
        return true;
    }
    
    /**#@+
     * Method group: Arbitrary javascript handling.
     */

    /**
     * Add arbitrary javascript to execute in ExtJS JS container
     * 
     * @param  string $js 
     * @return Losc_View_Helper_Ext
     */
    public function addJavascript($js)
    {
        $js = preg_replace('/^\s*(.*?)\s*$/s', '$1', $js);
        if (!in_array(substr($js, -1), array(';', '}'))) {
            $js .= ';';
        }

        if (in_array($js, $this->_javascriptStatements)) {
            return $this;
        }

        $this->_javascriptStatements[] = $js;
        return $this;
    }

    /**
     * Return all registered javascript statements
     * 
     * @return array
     */
    public function getJavascript()
    {
        return $this->_javascriptStatements;
    }

    /**
     * Clear arbitrary javascript stack
     * 
     * @return Losc_View_Helper_Ext
     */
    public function clearJavascript()
    {
        $this->_javascriptStatements = array();
        return $this;
    }

    /**
     * Capture arbitrary javascript to include in dojo script
     * 
     * @return void
     * @throws Losc_View_Exception  if trying to nest captures
     */
    public function javascriptCaptureStart()
    {
        if ($this->_captureLock) {
            throw new Losc_View_Exception('Cannot nest captures');
        }

        $this->_captureLock = true;
        return ob_start();
    }

    /**
     * Finish capturing arbitrary javascript to include in dojo script
     * 
     * @return true
     */
    public function javascriptCaptureEnd()
    {
        $data               = ob_get_clean();
        $this->_captureLock = false;

        $this->addJavascript($data);
        return true;
    }
    
    /**#@+
     * Method group: Stylesheet handling.
     */
 
    /**
     * Add a stylesheet
     * 
     * @para   string $file  name of css file to load
     * @param  string $path  [optional] if not given, Ext CSS path will be used
     * @return Losc_View_Helper_Ext
     */
    public function addStylesheet($file, $defaultPath = true)
    {
        if ($defaultPath) {
            $path = $this->getPath() . 'resources/css/';
            $file = $path . $file;
        }
        
        if (!in_array($file, $this->_stylesheets)) {
            $this->_stylesheets[] = $file;
        }
        
        return $this;
    }

    /**
     * Register the ExtJS ext-all.css stylesheet?
     *
     * With no arguments, returns the status of the flag; with arguments, sets 
     * the flag and returns the object.
     * 
     * @param  null|bool $flag
     * @return Losc_View_Helper_Ext|bool
     */
    public function registerExtStylesheet($flag = null)
    {
        if (null === $flag) {
             return $this->_registerExtStylesheet;
        }

        $this->_registerExtStylesheet = (bool) $flag;
        return $this;
    }

    /**
     * Retrieve registered stylesheets
     * 
     * @return array
     */
    public function getStylesheets()
    {
        return $this->_stylesheets;
    }
    
    /**#@+
     * Method group: Rendering.
     */

    /**
     * String representation of ExtJS environment
     * 
     * @param  string|int $indent  [optional] indentation to use, either a 
     *                             string, or number of spaces to use. defaults
     *                             to 8 spaces
     * @return string
     */
    public function toString($indent = null)
    {
        $indent = (null !== $indent)
                ? $this->_getWhitespace($indent)
                : $this->_getWhitespace(8);
        
        if (!$this->isEnabled()) {
            return PHP_EOL;
        }

        $html  = $this->_renderStylesheets($indent)
               . $this->_renderAdapterScriptTag($indent)
               . $this->_renderExtScriptTag($indent)
               . $this->_renderExtras($indent);
        return $html;
    }

    /**
     * Magic Method: String representation of ExtJS environment
     * 
     * @return string
     */
    public function __toString()
    {
        return $this->toString();
    }

    /**
     * Retrieve whitespace representation of $indent
     * 
     * @param  int|string $indent 
     * @return string
     */
    protected function _getWhitespace($indent)
    {
        if (is_int($indent)) {
            $indent = str_repeat(' ', $indent);
        }

        return (string) $indent;
    }

    /**
     * Render ExtJS stylesheets
     * 
     * @param  string $indent  [optional] indentation, defaults to ''
     * @return string
     */
    protected function _renderStylesheets($indent = '')
    {
        $path = $this->getPath() . 'resources/css/';
        $stylesheets = array();

        if ($this->registerExtStylesheet()) {
            $stylesheets[] = $path . 'ext-all.css';
        }

        foreach ($this->getStylesheets() as $stylesheet) {
            $stylesheets[] = $stylesheet;
        }

        if (empty($stylesheets)) {
            return '';
        }
        
        $style = $indent . '<style type="text/css" media="screen">' . PHP_EOL;
        foreach ($stylesheets as $stylesheet) {
            $style .= $indent . '    @import "' . $stylesheet . '";' . PHP_EOL;
        }
        $style .= $indent . '</style>' . PHP_EOL;

        return $style;
    }

    /**
     * Renders adapter script tag
     * 
     * @param  string $indent  [optional] indentation, defaults to ''
     * @return string
     */
    protected function _renderAdapterScriptTag($indent = '')
    {
        $path = $this->getPath() . 'adapter/';
        $adapter = $this->getAdapter();
        $files = array();
        
        switch ($adapter) {
            case self::ADAPTER_EXT:
                $files[] = $path . 'ext/ext-base.js';
                break;
            case self::ADAPTER_JQUERY:
                //$files[] = $path . 'jquery/jquery.js';
                $files[] = $path . 'jquery/ext-jquery-adapter.js';
                break;
            case self::ADAPTER_PROTOTYPE:
                //$files[] = $path . 'prototype/prototype.js';
                //$files[] = $path . 'prototype/scriptaculous.js';
                $files[] = $path . 'prototype/ext-prototype-adapter.js';
                break;
            case self::ADAPTER_YUI:
                $files[] = $path . 'yui/ext-yui-adapter.js';
                break;
        }
        
        $content = '';
        foreach ($files as $file) {
            $content .= $indent
                . '<script type="text/javascript" src="' . $file . '"></script>'
                      . PHP_EOL;
        }
        return $content;
    }

    /**
     * Renders ExtJS script tag
     * 
     * @param  string $indent  [optional] indentation, defaults to ''
     * @return string
     */
    protected function _renderExtScriptTag($indent = '')
    {
        $source = $this->getPath()
                . ($this->loadAll() ? 'ext-all' : 'ext-core')
                . ($this->loadDebug() ? '-debug.js' : '.js');

        return $indent .
               '<script type="text/javascript" src="' . $source . '"></script>'
               . PHP_EOL;
    }

    /**
     * Render ExtJS onReady stuff and arbitrary js junk
     * 
     * @param  string $indent  [optional] indentation, defaults to ''
     * @return string
     */
    protected function _renderExtras($indent = '')
    {
        $content = '';
    
        // oRready stuff
        foreach ($this->getOnReadyActions() as $callback) {
            $content .= $indent . '    Ext.onReady(' . $callback . ');' . PHP_EOL;
        }

        // arbitrary js junk
        foreach ($this->getJavascript() as $js) {
            $content .= PHP_EOL . $indent . '    ' . $js;
        }

        // return only line break if only whitespace
        if (preg_match('/^\s*$/s', $content)) {
            return PHP_EOL;
        }

        // return with indentation
        $html = $indent . '<script type="text/javascript">' . PHP_EOL
              . $indent . '    //<![CDATA[' . PHP_EOL
              .                $content . PHP_EOL
              . $indent . '    //]]>' . PHP_EOL
              . $indent . '</script>' . PHP_EOL;
        return $html;
    }
}
